package prefuse.data.expression;

import java.util.Comparator;

import prefuse.data.Schema;
import prefuse.data.Tuple;
import prefuse.util.TypeLib;
import prefuse.util.collections.DefaultLiteralComparator;
import prefuse.util.collections.LiteralComparator;

/**
 * Predicate instance that evaluates if a value is contained within
 * a bounded range.
 * 
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class RangePredicate extends BinaryExpression implements Predicate {

    /** Indicates the both the left and right bounds are inclusive */
    public static final int IN_IN = 0;
    /** Indicates an inclusive left bound and exclusive right bound */
    public static final int IN_EX = 1;
    /** Indicates an exclusive left bound and inclusive right bound */
    public static final int EX_IN = 2;
    /** Indicates the both the left and right bounds are exclusive */
    public static final int EX_EX = 3;

    private Expression m_middle;
    private Comparator m_cmp;

    // ------------------------------------------------------------------------
    // Constructors

    /**
     * Create a new RangePredicate. Both bounds are assumed to be inclusive.
     * @param middle the value to test for membership in the range
     * @param left the lower range bound
     * @param right the upper range bound
     */
    public RangePredicate(Expression middle, Expression left, Expression right) {
        this(IN_IN, middle, left, right, DefaultLiteralComparator.getInstance());
    }

    /**
     * Create a new RangePredicate. Both bounds are assumed to be inclusive.
     * @param middle the value to test for membership in the range
     * @param left the lower range bound
     * @param right the upper range bound
     * @param cmp the comparator to use for comparing data values
     */
    public RangePredicate(Expression middle, Expression left, Expression right, Comparator cmp) {
        this(IN_IN, middle, left, right, cmp);
    }

    /**
     * Create a new RangePredicate.
     * @param operation operation code indicating the inclusiveness /
     * exclusiveness of the bounds
     * @param middle the value to test for membership in the range
     * @param left the lower range bound
     * @param right the upper range bound
     */
    public RangePredicate(int operation, Expression middle, Expression left, Expression right) {
        this(operation, middle, left, right, DefaultLiteralComparator.getInstance());
    }

    /**
     * Create a new RangePredicate.
     * @param operation operation code indicating the inclusiveness /
     * exclusiveness of the bounds
     * @param middle the value to test for membership in the range
     * @param left the lower range bound
     * @param right the upper range bound
     * @param cmp the comparator to use for comparing data values
     */
    public RangePredicate(int operation, Expression middle, Expression left, Expression right, Comparator cmp) {
        super(operation, IN_IN, EX_EX, left, right);

        this.m_middle = middle;
        this.m_cmp = cmp;
    }

    // ------------------------------------------------------------------------
    // Accessors

    /**
     * Get the middle expression being tested for inclusion in the range
     * @return the middle expression
     */
    public Expression getMiddleExpression() {
        return m_middle;
    }

    /**
     * Get the comparator used to compare data values.
     * @return the comparator used to compare data values
     */
    public Comparator getComparator() {
        return m_cmp;
    }

    // ------------------------------------------------------------------------
    // Expression Interface

    /**
     * @see prefuse.data.expression.Expression#getBoolean(prefuse.data.Tuple)
     */
    public boolean getBoolean(Tuple t) {
        Class lType = m_left.getType(t.getSchema());
        Class rType = m_right.getType(t.getSchema());
        Class mType = m_middle.getType(t.getSchema());
        Class sType = null;

        // see if we can match the end-points' type
        if (lType.isAssignableFrom(rType)) {
            sType = lType;
        } else if (rType.isAssignableFrom(lType)) {
            sType = rType;
        }

        int c1, c2 = 0;
        if (sType != null && TypeLib.isNumericType(sType) && TypeLib.isNumericType(mType)) {
            // the range is of numeric types
            Class type = TypeLib.getNumericType(sType, mType);
            if (type == int.class) {
                int lo = m_left.getInt(t);
                int hi = m_right.getInt(t);
                int x = m_middle.getInt(t);
                c1 = ((LiteralComparator) m_cmp).compare(x, lo);
                c2 = ((LiteralComparator) m_cmp).compare(x, hi);
            } else if (type == long.class) {
                long lo = m_left.getLong(t);
                long hi = m_right.getLong(t);
                long x = m_middle.getLong(t);
                c1 = ((LiteralComparator) m_cmp).compare(x, lo);
                c2 = ((LiteralComparator) m_cmp).compare(x, hi);
            } else if (type == float.class) {
                float lo = m_left.getFloat(t);
                float hi = m_right.getFloat(t);
                float x = m_middle.getFloat(t);
                c1 = ((LiteralComparator) m_cmp).compare(x, lo);
                c2 = ((LiteralComparator) m_cmp).compare(x, hi);
            } else if (type == double.class) {
                double lo = m_left.getDouble(t);
                double hi = m_right.getDouble(t);
                double x = m_middle.getDouble(t);
                c1 = ((LiteralComparator) m_cmp).compare(x, lo);
                c2 = ((LiteralComparator) m_cmp).compare(x, hi);
            } else {
                throw new IllegalStateException();
            }
        } else {
            Object lo = m_left.get(t);
            Object hi = m_right.get(t);
            Object x = m_middle.get(t);
            c1 = m_cmp.compare(x, lo);
            c2 = m_cmp.compare(x, hi);
        }

        // check the comparison values to see if it is in-range
        switch (m_op) {
            case IN_IN:
                return (c1 >= 0 && c2 <= 0);
            case IN_EX:
                return (c1 >= 0 && c2 < 0);
            case EX_IN:
                return (c1 > 0 && c2 <= 0);
            case EX_EX:
                return (c1 > 0 && c2 < 0);
            default:
                throw new IllegalStateException("Unknown operation.");
        }
    }

    /**
     * @see prefuse.data.expression.Expression#getType(prefuse.data.Schema)
     */
    public Class getType(Schema s) {
        return boolean.class;
    }

    /**
     * @see prefuse.data.expression.Expression#get(prefuse.data.Tuple)
     */
    public Object get(Tuple t) {
        return (getBoolean(t) ? Boolean.TRUE : Boolean.FALSE);
    }

    /**
     * @see prefuse.data.expression.Expression#visit(prefuse.data.expression.ExpressionVisitor)
     */
    public void visit(ExpressionVisitor v) {
        v.visitExpression(this);
        v.down();
        m_left.visit(v);
        v.up();
        v.down();
        m_middle.visit(v);
        v.up();
        v.down();
        m_right.visit(v);
        v.up();
    }

    /**
     * @see prefuse.data.expression.AbstractExpression#addChildListeners()
     */
    protected void addChildListeners() {
        super.addChildListeners();
        m_middle.addExpressionListener(this);
    }

    /**
     * @see prefuse.data.expression.AbstractExpression#removeChildListeners()
     */
    protected void removeChildListeners() {
        super.removeChildListeners();
        m_middle.removeExpressionListener(this);
    }

    /**
     * @see java.lang.Object#toString()
     */
    public String toString() {
        String lop = "?", rop = "?";
        switch (m_op) {
            case IN_IN:
                lop = rop = "<=";
                break;
            case IN_EX:
                lop = "<=";
                rop = "<";
                break;
            case EX_IN:
                lop = "<";
                rop = "<=";
                break;
            case EX_EX:
                lop = rop = "<";
                break;
        }
        return '(' + m_left.toString() + ' ' + lop + ' ' + m_middle.toString() + " AND " + m_middle.toString() + ' ' + rop + ' ' + m_right.toString() + ')';
    }

} // end of class RangePredicate
